package backend;
import java.util.Vector;
import java.util.HashMap;
import java.util.Collection;
import java.util.List;
import java.io.*;
import java.security.MessageDigest;
import org.apache.commons.configuration.*;
import org.tinyradius.dictionary.*;
import org.tinyradius.attribute.*;

/**
 * This class represents a FreeRADIUS server being managed by FReeConf.
 *
 * @author Aaron A. Lovato
 * @author Charlie Moore
 */

public class Server
{
    // Dictionary needs a vendor number
    private final static int vendorNumber = 9999;

    // The name of this server
    private String hostname;

    // All users defined for this server
    private HashMap<String, User> users;

    // All roles defined for this server
    private HashMap<String, Role> roles;

    // The configuration files being managed by FReeConf
    private String usersFile;
    private String dictFile;
    private String localUsersFile;
    private String localDictFile;

    // The file containing the private key needed to access this server
    private String keyFile;

    // Username to use to access server
    private String user;

    // Configuration object for this server
    private XMLConfiguration config;

    /**
     * Create a new Server object.
     *
     * @param name The hostname of this server.
     */
    public Server(String name) throws ConfigurationException
    {
        this.hostname = name;
        this.users = new HashMap<String, User>();
        this.roles = new HashMap<String, Role>();

        // Give this server a configuration so we can update its properties
        String server_path = "src/config/"+name+".xml";
        this.config = new XMLConfiguration(new File(server_path));

        // Update the master server list
        XMLConfiguration server_list = new XMLConfiguration("src/config/servers.xml");
        server_list.addProperty("server(-1)[@filename]", name);
        server_list.save();
    }

    /**
     * Create a new Server object and populate it with our configuration values.
     *
     * @param config The configuration we're using to populate the Server's values.
     */
    public Server(XMLConfiguration c, String name)
    {
        this.localUsersFile = "src/config/users";
        this.localDictFile = "src/config/dictionary.rbac";
        this.config = c;
        this.hostname = name;
        this.user = this.config.getString("user");
        this.usersFile = this.config.getString("users-file");
        this.dictFile = this.config.getString("dict-file");
        this.keyFile = this.config.getString("key-file");
        this.users = new HashMap<String, User>();
        this.roles = new HashMap<String, Role>();

        // Get user properties in parallel, instantiate User objects for them
        // and stick them in our HashMap
        List username_list = this.config.getList("users.user[@name]");
        List userpass_list = this.config.getList("users.user[@pass]");
        for (int i=0; i < username_list.size(); i++) {
            this.users.put((String) username_list.get(i),
                           new User((String)username_list.get(i), (String)userpass_list.get(i)));
        }

        // Likewise instantiate the Roles
        List role_list = this.config.getList("roles.role[@name]");
        for (int i=0; i < role_list.size(); i++) {
            Role temp = new Role((String)role_list.get(i), this);

            // Add permissions to the role
            List perm_names = this.config.getList("roles.role("+i+").permissions.perm[@name]");
            List perm_values = this.config.getList("roles.role("+i+").permissions.perm[@value]");
            for (int j=0; j < perm_names.size(); j++) {
                try {
                    temp.addPermission((String)perm_names.get(j),(String)perm_values.get(j));
                }
                catch (Exception e) {
                    System.out.println("Failed at adding permission " + (String)perm_names.get(j) +
                                       "=" + (String)perm_values.get(j) + " to role " +
                                       temp.getName() + ": " + e.getMessage());
                }
            }
            this.roles.put(temp.getName(), temp);

            // Assign users to the role
            List assigned_users = this.config.getList("roles.role("+i+").users-assigned.user[@name]");
            for (int j=0; j < assigned_users.size(); j++) {
                try {
                    assignUser((String)assigned_users.get(j), temp.getName());
                }
                catch (Exception e) {
                    System.out.println("Failed at assigning user " + (String)assigned_users.get(j) +
                                       " to role " + temp.getName() + ": " + e.getMessage());
                }
            }
        }

        // Now that we've populated the roles, we have to do another pass for subroles
        for (int i=0; i < role_list.size(); i++) {
            Role temp = this.roles.get((String)role_list.get(i));
            List subrole_list = this.config.getList("roles.role("+i+").subroles.role[@name]");

            for (int j=0; j < subrole_list.size(); j++) {
                try {
                    temp.addSubRole(this.roles.get((String)subrole_list.get(j)));
                }
                catch (Exception error) {
                    System.out.println(error.getMessage());
                }
            }
        }
    }

    /**
     * Set the username that's used to access the server
     *
     * @param name The username in question
     */
    public void setUser(String name)
    {
        this.user = name;
        // this.config.setProperty("user", name);
    }

    /**
     * Set the path to the public key used by the server
     *
     * @param path A full path to the public key
     */
    public void setKeyFile(String path)
    {
        this.keyFile = path;
        // this.config.setProperty("key-file", path);
    }


    /**
     * Set the users files to be managed by FReeConf.
     *
     * @param remote Users file on this server to be managed.
     * @param local Location to store local copy of the users file.
     */
    public void setUsersFile(String remote)
    {
        this.usersFile = remote;
        // this.config.setProperty("users-file", remote);
    }

    /**
     * Set the directory dictionary files to be managed by FReeConf.
     *
     * @param local Location to store local copy of the dictionary file.
     */
    public void setDictFile(String remote)
    {
        this.dictFile = remote;
        // this.config.setProperty("dict-file", remote);
    }

    /**
     * Store a private key for accessing this server. Assumes that public key
     * authentication is set up for this server.
     *
     * @param key Public to use.
     * @param location Location to store this public key.
     * @param user User to use for authentication
     */
    //     public void saveKey(String key, String location, String user) throws
    //         IOException
    //     {
    //         JOptionPane.showMessageDialog(null, "Oh god how did this get here, I am not good with computer",
    //                                       "Don't ever do that again", JOptionPane.ERROR_MESSAGE, new ImageIcon("resources/cat.jpg"));
    //         FileWriter output = new FileWriter(location);
    //         output.write(key);
    //         output.flush();
    //         output.close();
    //         System.out.println("FFFFFFUUUUU");
    //
    //         this.user = user;
    //         this.config.setProperty("key-file", location);
    //     }

    /**
     * Add a role to this server.
     *
     * @param name Name to give this role.
     */
    public void addRole(String name) throws Exception
    {
        Role newRole = new Role(name, this);
        // We already have a role with this name
        if (this.roles.containsKey(name))
            throw new Exception("A role with this name already exists");
        System.out.println("added role: " + name);

        roles.put(name, newRole);
        // this.config.addProperty("roles.role(-1)[@name]", name);
    }

    /**
     * Add a subrole to a given role
     */
    public void addSubRole(String rolename, String subrolename) throws Exception
    {
        // Okay, don't check anything here because we're only called by reliable sources
        Role parent = this.roles.get(rolename);
        Role child = this.roles.get(subrolename);

        // This is only the beginning, we need to update permissions now
        parent.addSubRole(child);
        child.addParent(parent);

        // List t = this.config.getList("roles.role[@name]");
        // for (int i=0; i < t.size(); i++) {
        //     if (t.get(i) == rolename) {
        //         this.config.addProperty("roles.role("+i+").subroles.role(-1)[@name]", subrolename);
        //     }
        // }
    }

    /**
     * Remove a role from this server. Note that if there are users who are in
     * this role, but no others, they will become "orphaned."
     *
     * @param name The name of the role to remove.
     */
    public void removeRole(String name) throws Exception
    {
        // First make sure this role exists
        if (!this.roles.containsKey(name))
            throw new Exception("This role does not exist");

        this.roles.remove(name);

        // List t = this.config.getList("roles.role[@name]");
        // for (int i=0; i < t.size(); i++) {
        //     if (t.get(i) == name)
        //         this.config.clearProperty("roles.role("+i+")");
        // }
    }

    /**
     * Remove a subrole from a given role
     * @param rolename The name of the role we want to update.
     * @param subrolename The name of the subrole to remove.
     */
    public void removeSubRole(String rolename, String subrolename) throws Exception
    {
        Role parent = this.roles.get(rolename);
        Role child = this.roles.get(subrolename);

        parent.removeSubRole(child);
        child.removeParent(parent);

        // List t = this.config.getList("roles.role[@name]");
        // for (int i=0; i < t.size(); i++) {
        //     if (t.get(i) == rolename) {
        //         List subroles = this.config.getList("roles.role("+i+").subroles.role[@name]");
        //         for (int j=0; j < subroles.size(); i++) {
        //             if (subroles.get(j) == subrolename)
        //                 this.config.clearProperty("roles.role("+i+").subroles.role("+j+")");
        //         }
        //     }
        // }
    }


    /**
     * Assign a permission to a role.
     *
     * @param role Role to assign this permission to.
     * @param name Name of this permission.
     * @param value Value of this permission.
     */
    public void assignPermission(String role, String name, String value) throws Exception
    {
        // First make sure this role exists
        if (!this.roles.containsKey(role))
            throw new Exception("This role does not exist");

        this.roles.get(role).addPermission(name, value);

        // List t = this.config.getList("roles.role[@name]");
        // for (int i=0; i < t.size(); i++) {
        //     if (t.get(i) == role) {
        //         this.config.addProperty("roles.role("+i+").permissions.perm(-1)[@name]", name);
        //         this.config.addProperty("roles.role("+i+").permissions.perm[@value]", value);
        //     }
        // }
    }

    /**
     * Remove a permission from a given role.
     *
     * @param role Role to remove permission from.
     * @param name Name of the permission to remove.
     */
    public void removePermission(String role, String name) throws Exception
    {
        // Make sure this role exists
        if (!this.roles.containsKey(role))
            throw new Exception("This role does not exist");

        this.roles.get(role).removePermission(name);

        // List t = this.config.getList("roles.role[@name]");
        // for (int i=0; i < t.size(); i++) {
        //     if (t.get(i) == role) {
        //         System.out.println("Found role "+role);
        //         List m = this.config.getList("roles.role("+i+").permissions.perm[@name]");
        //         for (int j=0; j < m.size(); j++) {
        //             System.out.println("Looking at permission "+name);
        //             if (m.get(j) == name) {
        //                 System.out.println("roles.role("+i+").permissions.perm("+j+")");
        //                 this.config.clearProperty("roles.role("+i+").permissions.perm("+j+")");
        //             }
        //         }
        //     }
        // }
    }

    /**
     * Assign a user to a role
     *
     * @param name Username to modify.
     * @param role Role to assign this user to.
     */
    public void assignUser(String name, String role) throws Exception
    {
        // First make sure this role exists
        if (!this.roles.containsKey(role))
            throw new Exception("This role does not exist");

        // Check if this user already exists
        if (!this.users.containsKey(name))
            throw new Exception("This user does not exist");

        User tmp = this.users.get(name);
        this.roles.get(role).addUser(tmp);
        tmp.addRole(this.roles.get(role));
        // List t = this.config.getList("roles.role[@name]");
        // for (int i=0; i < t.size(); i++) {
        //     if (t.get(i) == role)
        //         this.config.addProperty("roles.role("+i+").users-assigned.user(-1)[@name]", name);
        // }
    }

    /**
     * Unassign a role from a user.
     *
     * @param role The name of the role to remove user from.
     * @param name The name of the user to remove from role.
     */
    public void unassignUser(String role, String name) throws Exception
    {
        // First make sure this role exists
        if (!this.roles.containsKey(role))
            throw new Exception("This role does not exist");

        // Check if this user already exists
        if (!this.users.containsKey(name))
            throw new Exception("This user does not exist");

        User user = this.users.get(name);
        this.roles.get(role).removeUser(user);

        // List t = this.config.getList("roles.role[@name]");
        // for (int i=0; i < t.size(); i++) {
        //     if (t.get(i) == role) {
        //         List m = this.config.getList("roles.role("+i+").users-assigned.user[@name]");
        //         for (int j=0; j < m.size(); j++) {
        //             if (m.get(j) == name)
        //                 this.config.clearProperty("roles.role("+i+").users-assigned.user("+j+")");
        //         }
        //     }
        // }
    }

    /**
     * Add a user to this server.
     *
     * @param name Username to add.
     * @param password Password to give user.
     */
    public void addUser(String name, String password) throws Exception
    {
        // Check if this user already exists
        if (this.users.containsKey(name))
            throw new Exception("This user already exists");

        User newUser = new User(name, password);
        this.users.put(name, newUser);

        // this.config.addProperty("users.user(-1)[@name]", name);
        // this.config.addProperty("users.user[@pass]", password);
    }

    /**
     * Set a user's password.
     *
     * @param name Username to modify.
     * @param password New password.
     */
    public void setPassword(String name, String password) throws Exception
    {
        // Make sure this users exists
        if (!this.users.containsKey(name))
            throw new Exception("This user does not exist");

        this.users.get(name).setPassword(password);

        // List t = this.config.getList("users.user[@name]");
        // for (int i=0; i < t.size(); i++) {
        //     if (t.get(i).equals(name))
        //         this.config.setProperty("users.user("+i+")[@pass]", password);
        // }
    }

    /**
     * Remove a user from this server.
     *
     * @param name Username to remove.
     */
    public void removeUser(String name) throws Exception
    {
        // Make sure this users exists
        if (!this.users.containsKey(name))
            throw new Exception("This user does not exist");

        // Get the user object
        User user = this.users.get(name);

        // Delete this user from all roles it may be in
        for(Role r : this.roles.values())
            r.removeUser(user);

        // Remove this user from the list of users
        this.users.remove(name);

        // List t = this.config.getList("users.user[@name]");
        // for (int i=0; i < t.size(); i++) {
        //     if (t.get(i).equals(name))
        //         this.config.clearProperty("users.user("+i+")");
        // }
    }

    /**
     * Write out the FreeRADIUS RBAC configuration. Note that this method does
     * not push the configuration to the server and will necessarily
     * overwrite the locally stored configuration files.
     */
    public void saveConfiguration() throws IOException
    {
        // Need to keep track of what number to map each attribute to
        int attributeNum = 1;

        // Create the local dictionary file
        FileWriter dictWriter = new FileWriter(this.localDictFile);

        // Write out vendor string
        dictWriter.write("VENDOR \t RBAC \t " + vendorNumber + "\n");
        dictWriter.write("BEGIN-VENDOR RBAC\n");
        // Go through all roles, writing out permissions
        for (Role r : roles.values())
        {
            for(Permission p : r.getPermissions().values())
            {
                String permString = "ATTRIBUTE ";
                permString += p.getName();
                permString += " ";
                permString += attributeNum;
                permString += " string\n";
                dictWriter.write(permString);
                attributeNum++;
            }
        }
        dictWriter.write("END-VENDOR RBAC\n");

        // Close dictionary
        dictWriter.flush();
        dictWriter.close();

        // Create local users file
        FileWriter userWriter = new FileWriter(this.localUsersFile);

        // Go through users list, writing out permissions for each role
        for(User u : users.values()) {
            // Write username and password
            String tmp = u.getName();
            tmp += " Cleartext-Password := ";
            tmp += u.getPassword();
            tmp += "\n";
            userWriter.write(tmp);

            // Get all permissions from all roles this user has
            for (Role r : u.getRoles()) {
                writeUserPermissions(r, userWriter);
//                 for(Permission p : r.getPermissions().values()) {
//                     String permString = "  ";
//                     permString += p.getName();
//                     permString += " = ";
//                     permString += p.getValue();
//                     permString += ",\n";
//                     userWriter.write(permString);
//                 }
            }
        }

        // Close users file
        userWriter.flush();
        userWriter.close();
    }
    
    private void writeUserPermissions(Role r, FileWriter userWriter) throws IOException
    {
        for(Permission p : r.getPermissions().values()) {
            String permString = "  ";
            permString += p.getName();
            permString += " = \"";
            permString += p.getValue();
            permString += "\",\n";
            userWriter.write(permString);
        }
        for (Role role : r.getParents())
        {
            writeUserPermissions(role, userWriter);
        }
    }


    /**
     * Save the configuration to our xml config file
     */
    public void saveAppConfiguration() {
        // Rebuild the whole damn file because clearProperty is a bitch
        this.config.clear();
        XMLConfiguration xml = new XMLConfiguration();
        xml.setFile(this.config.getFile());

        // Write out the first part manually because addProperty is too stupid
        try {
            FileWriter file = new FileWriter(xml.getFile());
            file.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
                       "<server hostname=\""+this.hostname+"\" username=\""+this.user+"\">\n" +
                       "</server>");
            file.flush();
            xml.load();
        }
        catch (Exception e) {
            System.out.println("Kill me: " + e.getMessage());
        }

        // Add all the single string values
        xml.addProperty("user", this.user);
        xml.addProperty("users-file", this.usersFile);
        xml.addProperty("dict-file", this.dictFile);
        xml.addProperty("local-users-file", this.localUsersFile);
        xml.addProperty("local-dict-file", this.localDictFile);
        xml.addProperty("key-file", this.keyFile);

        // Add the list of users
        for (String username : this.users.keySet()) {
            User user = this.users.get(username);
            xml.addProperty("users.user(-1)[@name]", user.getName());
            xml.addProperty("users.user[@pass]", user.getPassword());
        }

        // Add the roles
        for (String rolename : this.roles.keySet()) {
            Role role = this.roles.get(rolename);
            xml.addProperty("roles.role(-1)[@name]", rolename);

            // Add user assignments
            for (String user : role.getUsers().keySet()) {
                xml.addProperty("roles.role.users-assigned.user(-1)[@name]", user);
            }

            // Add permissions
            for (String perm : role.getPermissions().keySet()) {
                Permission p = role.getPermissions().get(perm);
                String value = p.getValue();
                value = value.replace(",", "\\,");
                xml.addProperty("roles.role.permissions.perm(-1)[@name]", perm);
//                 xml.addProperty("roles.role.permissions.perm[@value]", p.getValue());
                xml.addProperty("roles.role.permissions.perm[@value]", value);
            }

            // Add subroles
            for (Role r : role.getSubRoles()) {
                xml.addProperty("roles.role.subroles.role(-1)[@name]", r.getName());
            }
        }

        // Now save that fucking shit
        try {
            xml.save();
        }
        catch (Exception e) {
            System.out.println("Saving the config fucked up: " + e.getMessage());
        }
    }

    /**
     * Push local configuration files to the server.
     */
    public void pushConfiguration() throws IOException
    {
        // Save local files
        this.saveAppConfiguration();
        this.saveConfiguration();

        // Copy the dictionary fle
        String command = "scp -o PreferredAuthentications=publickey -i ";
        command += this.keyFile + " " + localDictFile + " " + user;
        command += "@" + hostname + ":" + dictFile;
        System.out.println(command);
        Runtime r = Runtime.getRuntime();
        Process p;
        try {
            p = r.exec(command);
            p.waitFor();
            System.out.println("returned " + p.exitValue());
        }
        catch(Exception e)
        {
            System.out.println("OH NOES!!");
        }


        // Copy the user file
        command = "scp -o PreferredAuthentications=publickey -i ";
        command += this.keyFile + " " + localUsersFile + " " + user;
        command += "@" + hostname + ":" + usersFile;
        System.out.println(command);
        try {
            p = r.exec(command);
            p.waitFor();
            System.out.println("returned " + p.exitValue());
        }
        catch(Exception e)
        {
            System.out.println("OH NOES!!");
        }
    }

    /**
     * Verify configuration files on the server.
     * @return Indicates whether configuration files match
     */
    public boolean verifyConfiguration() throws Exception
    {
        saveConfiguration();

        Runtime r = Runtime.getRuntime();
        Process p;

        // Grab configuration files from server
        String tempfile = "src/config/tmp";
        // Copy the user file
        String command = "scp -o PreferredAuthentications=publickey -i ";
        command += this.keyFile + " " + user;
        command += "@" + hostname + ":" + usersFile + " " + tempfile;
        System.out.println(command);
        try {
            p = r.exec(command);
            p.waitFor();
            System.out.println("returned " + p.exitValue());
        }
        catch(Exception e)
        {
            System.out.println("OH NOES!!");
        }

        // Compare hashes
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        FileInputStream fis = new FileInputStream(tempfile);

        byte[] dataBytes = new byte[1024];

        int nread = 0;
        while ((nread = fis.read(dataBytes)) != -1) {
            md.update(dataBytes, 0, nread);
        };
        byte[] mdbytes = md.digest();

        //convert the byte to hex format method 1
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < mdbytes.length; i++) {
            sb.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1));
        }

        String serverUsersHash = sb.toString();
        fis.close();


        fis = new FileInputStream(localUsersFile);

        nread = 0;
        while ((nread = fis.read(dataBytes)) != -1) {
            md.update(dataBytes, 0, nread);
        };
        mdbytes = md.digest();

        //convert the byte to hex format method 1
        sb = new StringBuffer();
        for (int i = 0; i < mdbytes.length; i++) {
            sb.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1));
        }

        String localUsersHash = sb.toString();


        if (serverUsersHash.compareTo(localUsersHash) != 0)
            return false;

        return true;
    }

    /**
     * Get a Vector containing all users on this server.
     * @return All users on this server.
     */
    public HashMap<String, User> getUsers()
    {
        return users;
    }

    /**
     * Get a Vector containing all user names on this server.
     * @return All user names on this server.
     */
    public Vector<String> getUserNames()
    {
        Vector<String> usernames = new Vector<String>(this.users.keySet());
        return usernames;
    }

    /**
     * Get a HashMap containing all roles on this server.
     * @return All roles on this server.
     */
    public HashMap<String, Role> getRoles()
    {
        return roles;
    }

    /**
     * Get a Vector containing all role names on this server.
     * @return All role names on this server.
     */
    public Vector<String> getRoleNames()
    {
        Vector<String> rolenames = new Vector<String>(this.roles.keySet());
        return rolenames;
    }
}
